feat(ia): 备考域重布局 + catalog/成绩第一刀 - #25
Conversation
- test_audit_hardening: clean_db fixture 前后钉 DATABASE_PATH/PROGRESS_DB_PATH, 防全量 pytest 时 test_server.py 顶层改写 env 导致本文件命中未建表的默认库。 - test_server: import server 改 import nlp(9fea63e 已剔除 server 侧 nlp 透传, 旧引用是死测路径)。 - docs(plans): ADR-0005 Phase1 实施计划入库(词表全迁/静态导航/题库不入库三决策版)。
- 桌面 nav + 移动 dock 静态加「备考 (PRÜFUNG)」双端入口(nav-btn-exam/mob-btn-exam)。
- 新建 view-exam 壳:exam-level-tabs(A1 占位) + exam-module-grid 五模块占位卡片
(写作/听力/阅读/口语/词表)——置于 view-german 之前不破 wb split 切块断言。
- style.css 新增 .exam-* 最小样式(复用既有 paper 体系变量,纯插入)。
- show('exam') 零 JS 改动(main.js view-id 惯例天然点亮)。
- test_exam_domain.py 11 条契约测试(含顺序/配平/死测防恒真切片)。
- 定向回归 128 passed(基线 117 + 新增 11)。
- view-exam 收拢五面板:写作(exam-writing + setExamWritingTab 页签)、 词表/口语(exam-cards-container + 自管 toggle)、听力/阅读(原容器 id 搬入)。 - view-writer 回归纯 essay(writer-mode-a1-* 删除,switchWriterMode 收敛 no-op 防悬空); view-cards 回归纯复习四段(seg-a1 删除、renderCardsGrid a1 分流删净)。 - a1_cards.js 渲染目标 a1CardsHost() 改指 exam 容器(回退主站 cards-container 即探针红); setA1Mode 加冷缓存懒加载守卫(等待期 mode 竞态防护)。 - main.js show() 守卫改 view !== "exam"(停考计时器)、exam 分支 loadCards + setExamModule mediator(页签⇄面板路由);exposer 补挂 3 函数(含 import 链核对)。 - 新探针 tools/ia_dom_mount_probe.mjs(node:vm 真跑 4 场景,含变异验证 7 条回退必红)。 - 测试同步:seg-a1→exam-card-vocab、writer-mode-a1-*→exam-tab-*(ledger 登记项)。 - 回归 537 passed(基线 518+11+8)+ 10/10 探针全绿;评审 PASS(悬空标识符/双现/探针质量核查)。
- exam_catalog.py 单源注册目录(EXAM_CATALOG,count_fn 数据推导:writing 18/
hoeren 5/lesen 6/sprechen 54/vocab 702),_safe_count 抛错记 0 + logger.warning
留痕(零静默吞异常,caplog 钉死);docstring 如实标注模块级 import 硬依赖面。
- routes_exam.py GET /api/exams/catalog 纯只读端点;server.py include 一行接入。
- main.js initExamCatalog:fetch catalog → 卡片标题/count 徽标/新等级页签;
失败静态回退 + 幂等守卫 + show('exam') 惰性触发;非静态新页签标注「待接入」
(no-op + aria-disabled,防死按钮 v5.1.0 前科)。
- 加级成本已验证:插一行 A2 → catalog 多一级(变异断言钉死扩展点)。
- 旧 /api/a1/* 端点零改动(6 端点 smoke 锚);定向回归 37 passed。
- 评审 PASS(初轮 REVISE 两处已修:吞异常 + 死代码)。
- progress.db 新增 exam_trials(level,module,set_id,score_raw,…) 泛化成绩表; record_exam_trial/get_exam_history 泛化读写(9 列投影,无 level/module 冗余泄漏)。 - 旧 record_a1_hoeren_trial/record_a1_lesen_trial/get_a1_hoeren_history/get_a1_lesen_history 改内部透传(签名+返回结构不变,调用方零改动)。 - migrate_a1_records_to_exam_trials() 幂等迁移:行数对账谓词用 >=(非 ==)—— == 在「透传后旧表冻结+新表随成绩单调增,迁移后又做一次成绩再重启」场景 general=legacy+1 误判未迁移→整表重插(已复现 count 3→5,回归测试钉死)。 db_progress_conn 单事务保证半迁回滚;migrate 每次启动跑,增/幂等皆安全。 - 备份接线:_PROGRESS_TABLES 带 exam_trials + RestoreReq.exam_trials 字段 + restore 灌表 + restore 后幂等迁移(v5.1 旧备份无 exam_trials 键时历史仍可见)。 - test_server.py clean_db env 双钉(防收集顺序导致未建表库命中)。 - 回归 287 passed(含新增 11 条 exam_trials 契约测试 + 迁移重入回归); 评审 PASS(幂等/透传契约/init 无递归/备份链/SQL 参数化逐项核验)。
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed functional and data-integrity issues in the changed code (HTML markup breakage, exam-domain A1 toolbar visibility, and incorrect A1 event_type mapping) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR implements ADR-0005 Phase 1 by introducing an explicit “Exam Domain” (备考域) in the frontend navigation, moving Goethe A1 learning modules out of tool views into that domain, and adding the first “catalog + generalized trials” backend primitives to make levels/modules scalable.
Changes:
- Frontend: add
view-examand new nav/dock entry; migrate A1 writing/listening/reading/speaking/vocab UI to the exam domain with a root-module mediator (setExamModule) and a DOM+behavior probe (ia_dom_mount_probe.mjs). - Backend: add
/api/exams/catalogdriven by a code-registered catalog (exam_catalog.py) and generalize A1 exam results intoexam_trialswith idempotent migration + backup/restore wiring. - Tests/docs: substantial new regression coverage for exam-domain DOM wiring, catalog contract, trials migration/backup, plus README + plan/ledger updates.
File summaries
| File | Description |
|---|---|
| tools/ia_dom_mount_probe.mjs | New node:vm behavioral probe to ensure migrated A1 panels mount into view-exam and render into the correct containers. |
| test_server.py | Hardens DB env isolation in fixtures; updates NLP tests to import from nlp instead of server. |
| test_goethe_a1.py | Updates A1 frontend DOM assertions to match the new exam-domain entry point. |
| test_goethe_a1_writing.py | Updates writing-workshop DOM assertions for the exam-domain tabs (exam-tab-*). |
| test_exam_trials.py | New test suite for generalized exam_trials, legacy pass-through, idempotent migration, and backup/restore wiring. |
| test_exam_domain.py | New static + dynamic (probe-driven) contract tests for the exam-domain nav/view structure and routing. |
| test_exam_catalog.py | New tests for /api/exams/catalog contract, extension behavior, failure resilience, and legacy /api/a1/* smoke. |
| test_audit_hardening.py | Mirrors the “pin env per test” fixture discipline to avoid collection-order DB contamination. |
| static/style.css | Adds minimal styling for the new exam-domain layout (level tabs, module card grid, count badge). |
| static/js/writer.js | Re-exports setExamWritingTab via a1_writer.js for the new exam-domain mediator wiring. |
| static/js/main.js | Updates show() for exam-domain lifecycle; adds setExamModule mediator; adds catalog lazy init and UI enrichment. |
| static/js/cards.js | Removes the a1 segment handling from cards view (A1 now owned by exam domain). |
| static/js/a1_writer.js | Refactors A1 writing switching into setExamWritingTab; keeps switchWriterMode() as a no-op to avoid dangling references. |
| static/js/a1_cards.js | Redirects A1 rendering to #exam-cards-container; adds exam-local view-mode state + lazy-load guard. |
| static/index.html | Adds nav/dock “Exam” entry and new view-exam; moves A1 module DOM into exam panels; removes legacy A1 UI from tool views. |
| server.py | Includes the new exam router; extends restore payload with exam_trials and triggers post-restore A1→exam_trials migration. |
| routes_exam.py | New router providing /api/exams/catalog. |
| README.md | Updates test badge count and documents ADR-0005 Phase 1 changes for v5.2.0. |
| exam_catalog.py | New code-registered exam catalog with safe count derivation and warning logging on failures. |
| docs/plans/2026-09-04-ia-nav-exam-domain-phase1.md | New/updated implementation plan document for ADR-0005 Phase 1. |
| docs/plans/2026-09-04-ia-nav-exam-domain-phase1-ledger.md | New/updated execution ledger with verification evidence and deltas. |
| database.py | Adds exam_trials table, generalized record/history helpers, idempotent A1 migration, and backup table spec wiring. |
Review details
- Files reviewed: 22/22 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if level == "A1": | ||
| event_type = "a1_hoeren" if module == "hoeren" else "a1_lesen" | ||
| else: | ||
| event_type = f"{level.lower()}_{module}" |
| <a | ||
| href="/api/a1/export/anki" | ||
| class="btn btn-ghost btn-xs a1-anki-export-btn" | ||
| title="一键导出官方歌德 A1 词表到 Anki 牌组" | ||
| >📥 导出 A1 牌组</a | ||
| > | ||
| </div> |
| const searchRow = document.getElementById("a1-search-row"); | ||
| const pillsRow = document.getElementById("a1-topic-pills"); | ||
| const viewToggle = document.querySelector(".cards-view-toggle"); | ||
| const cardsContainer = document.getElementById("cards-container"); | ||
| const viewToggle = document.getElementById("exam-cards-view-toggle"); | ||
| const cardsContainer = a1CardsHost(); | ||
| const hoerenContainer = document.getElementById("a1-hoeren-container"); |
| btn.textContent = lv.title || lv.id; | ||
| btn.title = "该等级模块待接入"; | ||
| btn.setAttribute("aria-disabled", "true"); | ||
| btn.onclick = () => {}; // no-op:防静默死按钮,待该等级模块接线后替换 | ||
| tabs.appendChild(btn); |
feat(ia): 备考域重布局 + catalog/成绩第一刀(ADR-0005 Phase 1)
按 ADR-0005(
d:/Obsidian/Coding/08-Projects/DeLector/01-ADR/0005-navigation-exam-domain-and-level-scalability.md)把 DeLector 前端从「工具混入考纲素材」重排为「场景工具 + 独立备考域」,并落地目录/成绩泛化第一刀。关键决策(vault-grill 拍板)
变更
tools/ia_dom_mount_probe.mjs行为级 DOM 探针(node:vm 真跑,回退必红)/api/exams/catalog目录化端点到前端数据驱动exam_trials(level,module,…)泛化成绩表 + 幂等迁移 A1 存量 + 备份/还原接线;旧a1_hoeren_records/a1_lesen_records保留兼容期测试证据
==→>=(透传模式「迁移后又做成绩再重启」会整表重插,已复现 count 3→5,回归测试钉死)范围外(后继触发点,见计划 §9)
/api/a1/*全量切/api/exams/{level}/{module}、旧 a1 端点/表退役 → A2 立项时计划 / ledger
docs/plans/2026-09-04-ia-nav-exam-domain-phase1.mddocs/plans/2026-09-04-ia-nav-exam-domain-phase1-ledger.md